"""
    A layer between SQL and python ORM.
"""

import threading, dispatch
import cherrypy, turbogears
from cherrypy.filters.basefilter import BaseFilter
from sqlobject import events, classregistry, col
from sqlobject.include.pydispatch import dispatcher
from mnemoply import model
from mnemoply.model.columns import Gender, SOGenderCol, SOTariffCol
from mnemoply.controllers.root import Root
from mnemoply.utils import md5, date2str_points, replace_empty_to_none

def get_class_repr(cls):
    name = cls.__name__

    candidates = list(model.AuditObjectClass.selectBy(name=name))
    if not len(candidates):
        return model.AuditObjectClass(name=name)
    else:
        return candidates[0]

def get_url_repr(url):
    candidates = list(model.AuditActionUrl.selectBy(url=url))
    if not len(candidates):
        return model.AuditActionUrl(url=url)
    else:
        return candidates[0]

transactions = threading.local()

def get_current_transaction():
    try:
        return transactions.current
    except AttributeError:
        return None

def set_current_transaction(value):
    transactions.current = value

def get_or_create_current_transaction():
    t = get_current_transaction()
    if t is None:
        t = model.AuditTransactionRecord(
            user=turbogears.identity.current.user,
            actionUrl=get_url_repr(cherrypy.request.path),
        )

        set_current_transaction(t)

    return t

@dispatch.generic()
def obj2text(column, value):
    pass

@obj2text.when("isinstance(column, SOGenderCol) and value is not None")
def obj2text_gender_col(column, value):
    return (value == Gender.MALE) and 'Male' or 'Female'

@obj2text.when("isinstance(column, col.SOBoolCol) and value is not None")
def obj2text_boolean_col(column, value):
    return value and 'True' or 'False'

@obj2text.when("isinstance(column, col.SOKeyCol) and value is not None")
def obj2text_fkey_col(column, value):
    fkey_class = classregistry.findClass(column.foreignKey)
    obj = fkey_class.get(int(value))
    return "<%s>" % obj.title

@obj2text.when("isinstance(column, col.SODateCol) and value is not None")
def obj2text_date_col(column, value):
    return date2str_points(value)

@obj2text.when("isinstance(column, col.SOBLOBCol) and value is not None")
def obj2text_blob_col(column, value):
    if not len(value.strip()):
        return "BLOB:empty"
    return "BLOB:md5(%s)" % md5(value)

@obj2text.when("isinstance(column, SOTariffCol) and value is not None")
def obj2text_date_col(column, value):
    return "[%s]" % ", ".join([str(x) for x in value])

@obj2text.when(dispatch.strategy.default)
def obj2text_generic_col(column, value):
    if value is None:
        return "None"
    return '"%s"' % str(value)

def dump_obj_details(obj, skip_empty):
    classname = obj.__class__.__name__

    details = []
    for colname, col in obj.sqlmeta.columns.items():
        value = getattr(obj, colname)

        if colname.startswith("cache"):
            continue

        text = obj2text(col, value)
        if (not skip_empty) or (text and text != '""' and text != "None"):
            details.append("%s=%s" % (colname, text))

    return "\n".join(details)

def on_insert(kw, post_funcs):
    cls, id = kw['class'], kw['id']

    obj = cls.get(id)

    model.AuditChange(
        transaction=get_or_create_current_transaction(),
        kind='I',
        objectTitle=obj.title,
        objectClass=get_class_repr(cls),
        objectId=id,
        details=dump_obj_details(obj, True),
    )

def on_update(obj, d):
    classname = obj.__class__.__name__

    details = []
    d = replace_empty_to_none(d)
    for colname, new_value in d.items():

        col = obj.sqlmeta.columns[colname]
        old_value = getattr(obj, colname)

        old_text = obj2text(col, old_value)
        new_text = obj2text(col, new_value)

        if old_text != new_text:
            details.append("%s=%s (was %s)" % (colname, new_text, old_text))

    if not len(details):
        return

    model.AuditChange(
        transaction=get_or_create_current_transaction(),
        kind='U',
        objectTitle=obj.title,
        objectClass=get_class_repr(obj.__class__),
        objectId=obj.id,
        details="\n".join(details),
    )

def on_delete(obj, post_funcs):
    model.AuditChange(
        transaction=get_or_create_current_transaction(),
        kind='D',
        objectTitle=obj.title,
        objectClass=get_class_repr(obj.__class__),
        objectId=obj.id,
        details=dump_obj_details(obj, False),
    )

class ClearCurrentTransactionFilter(BaseFilter):
    def on_start_resource(self):
        set_current_transaction(None)

def tracked_class(class_name):
    return not class_name.startswith('Audit') and \
        not class_name.startswith('Visit')

def install_hooks():
    tables = [getattr(model, name) for name in model.soClasses if tracked_class(name)]

    for cls in tables:
        events.listen(on_insert, cls, events.RowCreatedSignal, weak=False)
        events.listen(on_update, cls, events.RowUpdateSignal, weak=False)
        events.listen(on_delete, cls, events.RowDestroySignal, weak=False)

def remove_hooks():
    tables = [getattr(model, name) for name in model.soClasses if tracked_class(name)]

    for cls in tables:
        dispatcher.disconnect(on_insert, events.RowCreatedSignal, cls, weak=False)
        dispatcher.disconnect(on_update, events.RowUpdateSignal, cls, weak=False)
        dispatcher.disconnect(on_delete, events.RowDestroySignal, cls, weak=False)

def install_filter():
    root_controller = Root.instance()

    filters = getattr(root_controller, '_cp_filters', [])
    filters.append(ClearCurrentTransactionFilter())
    setattr(root_controller, '_cp_filters', filters)

def initialize_audit():
    install_filter()
    install_hooks()

